package carousel

import (
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/charmbracelet/x/ansi"

	"github.com/dlvhdr/gh-dash/v4/internal/tui/constants"
)

// Model defines a state for the carousel widget.
type Model struct {
	KeyMap KeyMap

	items                  []string
	cursor                 int
	width                  int
	height                 int
	focus                  bool
	showOverflowIndicators bool
	leftOverflowIndicator  string
	rightOverflowIndicator string
	showSeparators         bool
	separator              string
	styles                 Styles

	content string
	start   int
	end     int
}

// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
// is used to render the menu.
type KeyMap struct {
	SelectLeft  key.Binding
	SelectRight key.Binding
}

// DefaultKeyMap returns a default set of keybindings.
func DefaultKeyMap() KeyMap {
	return KeyMap{
		SelectLeft: key.NewBinding(
			key.WithKeys("left", "h"),
			key.WithHelp("←/h", "h"),
		),
		SelectRight: key.NewBinding(
			key.WithKeys("right", "l"),
			key.WithHelp("→/l", "right"),
		),
	}
}

// Styles contains style definitions for this carousel component. By default,
// these values are generated by DefaultStyles.
type Styles struct {
	Item              lipgloss.Style
	Selected          lipgloss.Style
	OverflowIndicator lipgloss.Style
	Separator         lipgloss.Style
}

// DefaultStyles returns a set of default style definitions for this carousel.
func DefaultStyles() Styles {
	return Styles{
		Item: lipgloss.NewStyle().Padding(0, 1),
		Selected: lipgloss.NewStyle().
			Padding(0, 1).
			Foreground(lipgloss.Color("212")),
	}
}

// SetStyles sets the table styles.
func (m *Model) SetStyles(s Styles) {
	m.styles = s
	m.UpdateSize()
}

// Option is used to set options in New. For example:
//
//	carousel := New(WithItems([]string{"Item 1", "Item 2", "Item 3"}))
type Option func(*Model)

// New creates a new model for the carousel widget.
func New(opts ...Option) Model {
	m := Model{
		cursor: 0,

		KeyMap:                 DefaultKeyMap(),
		styles:                 DefaultStyles(),
		leftOverflowIndicator:  "<",
		rightOverflowIndicator: ">",
		separator:              "|",
	}

	for _, opt := range opts {
		opt(&m)
	}

	m.UpdateSize()

	return m
}

// WithItems sets the carousel items (data).
func WithItems(items []string) Option {
	return func(m *Model) {
		m.SetItems(items)
	}
}

// WithHeight sets the height of the carousel.
func WithHeight(h int) Option {
	return func(m *Model) {
		m.height = h
	}
}

// WithWidth sets the width of the carousel.
func WithWidth(w int) Option {
	return func(m *Model) {
		m.width = w
	}
}

// WithOverflowIndicators sets indicators that show up when the carousel items
// don't fit in the given width
func WithOverflowIndicators(indicators ...string) Option {
	return func(m *Model) {
		m.showOverflowIndicators = true
		if len(indicators) > 0 {
			m.leftOverflowIndicator = indicators[0]
		}
		if len(indicators) > 1 {
			m.rightOverflowIndicator = indicators[1]
		}
	}
}

// WithWidth sets the width of the carousel.
func WithSeparators(sep ...string) Option {
	return func(m *Model) {
		m.showSeparators = true
		if len(sep) > 0 {
			m.separator = sep[0]
		}
	}
}

// WithFocused sets the focus state of the carousel.
func WithFocused(f bool) Option {
	return func(m *Model) {
		m.focus = f
	}
}

// WithStyles sets the carousel styles.
func WithStyles(s Styles) Option {
	return func(m *Model) {
		m.styles = s
	}
}

// WithKeyMap sets the key map.
func WithKeyMap(km KeyMap) Option {
	return func(m *Model) {
		m.KeyMap = km
	}
}

// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	if !m.focus {
		return m, nil
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.KeyMap.SelectLeft):
			m.MoveLeft()
		case key.Matches(msg, m.KeyMap.SelectRight):
			m.MoveRight()
		}
	}

	return m, nil
}

// Focused returns the focus state of the carousel.
func (m Model) Focused() bool {
	return m.focus
}

// Focus focuses the carousel, allowing the user to move around the items and
// interact.
func (m *Model) Focus() {
	m.focus = true
	m.UpdateSize()
}

// Blur blurs the carousel, preventing selection or movement.
func (m *Model) Blur() {
	m.focus = false
	m.UpdateSize()
}

// View renders the component.
func (m Model) View() string {
	// d := lipgloss.JoinHorizontal(lipgloss.Center, m.content, fmt.Sprintf("cursor=%d", m.cursor))
	// return d
	return m.content
}

// UpdateSize updates the carousel size based on the previously defined
// items and width.
func (m *Model) UpdateSize() {
	leftover := m.width
	itemsContent := ""

	currDirection := -1
	left := m.cursor
	lastLeft := left
	right := min(m.cursor+1, len(m.items))
	lastRight := right
	for len(m.items) > 0 && leftover > 0 && (left >= 0 || right < len(m.items)) {
		if currDirection < 0 && left >= 0 {
			lItem := m.renderItem(left, leftover)
			leftover -= lipgloss.Width(lItem)
			itemsContent = lipgloss.JoinHorizontal(lipgloss.Top, lItem, itemsContent)
			lastLeft = left
			left--
		} else if currDirection > 0 && right < len(m.items) {
			rItem := m.renderItem(right, leftover)
			leftover -= lipgloss.Width(rItem)
			itemsContent = lipgloss.JoinHorizontal(lipgloss.Top, itemsContent, rItem)
			lastRight = right
			right++
		}

		if left < 0 {
			currDirection = 1
		} else if right > len(m.items)-1 {
			currDirection = -1
		} else {
			currDirection = currDirection * -1
		}
	}
	lastRight = min(lastRight, len(m.items)-1)

	m.start = lastLeft
	m.end = lastRight

	l := m.width
	loIndicator, roIndicator := "", ""

	if m.showOverflowIndicators && lastLeft != 0 {
		loIndicator = m.styles.OverflowIndicator.Render(m.leftOverflowIndicator)
		l -= lipgloss.Width(loIndicator)
	}
	if m.showOverflowIndicators && lastRight != len(m.items)-1 {
		roIndicator = m.styles.OverflowIndicator.Render(m.rightOverflowIndicator)
		l -= lipgloss.Width(roIndicator)
	}

	if loIndicator != "" {
		truncate := lipgloss.Width(itemsContent) - l + 1
		itemsContent = ansi.TruncateLeft(itemsContent, truncate, "")
		if truncate > 0 {
			itemsContent = lipgloss.JoinHorizontal(lipgloss.Center,
				m.styles.Item.Inline(true).Render(constants.Ellipsis), itemsContent)
		}
	} else {
		w := lipgloss.Width(itemsContent)
		if w > l {
			itemsContent = ansi.Truncate(itemsContent, l, "")
			itemsContent = lipgloss.JoinHorizontal(lipgloss.Center, itemsContent,
				m.styles.Item.Inline(true).Render(constants.Ellipsis))
		}
	}

	m.content = lipgloss.NewStyle().Height(m.height).Render(
		lipgloss.JoinHorizontal(lipgloss.Center, loIndicator, itemsContent, roIndicator))
}

// SelectedItem returns the selected item.
func (m Model) SelectedItem() string {
	return m.items[m.cursor]
}

// Items returns the current items.
func (m Model) Items() []string {
	return m.items
}

// SetItems sets a new items state.
func (m *Model) SetItems(items []string) {
	m.items = items
	m.cursor = clamp(m.cursor, 0, len(m.items)-1)
	m.start = 0
	m.UpdateSize()
}

// SetWidth sets the width of the carousel.
func (m *Model) SetWidth(w int) {
	m.width = w
	m.UpdateSize()
}

// SetHeight sets the height of the carousel.
func (m *Model) SetHeight(h int) {
	m.height = h
	m.UpdateSize()
}

// Height returns the height of the carousel.
func (m Model) Height() int {
	return m.height
}

// Width returns the width of the carousel.
func (m Model) Width() int {
	return m.width
}

// Cursor returns the index of the selected row.
func (m Model) Cursor() int {
	return m.cursor
}

// HasRightItems returns true if there's items left on the right.
func (m Model) HasRightItems() bool {
	return m.end < len(m.items)
}

// HasLeftItems returns true if there's items left on the left.
func (m Model) HasLeftItems() bool {
	return m.start > 0
}

// SetCursor sets the cursor position in the carousel.
func (m *Model) SetCursor(n int) {
	m.cursor = clamp(n, 0, len(m.items)-1)
	m.UpdateSize()
}

// MoveLeft moves the selection left by one item..
// It can not go before the first item.
func (m *Model) MoveLeft() {
	m.cursor = clamp(m.cursor-1, 0, len(m.items)-1)
	m.UpdateSize()
}

// MoveDown moves the selection right by one item.
// It can not go after the last row.
func (m *Model) MoveRight() {
	m.cursor = clamp(m.cursor+1, 0, len(m.items)-1)
	m.UpdateSize()
}

func (m *Model) renderItem(itemID int, maxWidth int) string {
	var item string
	if itemID == m.cursor {
		item = m.styles.Selected.Render(m.items[itemID])
	} else if itemID < m.cursor {
		r := m.styles.Item.Render(m.items[itemID])
		truncate := lipgloss.Width(r) - maxWidth - 1
		item = ansi.TruncateLeft(r, truncate, "")
		if truncate > 0 {
			item = lipgloss.JoinHorizontal(lipgloss.Center,
				m.styles.Item.Inline(true).Render(constants.Ellipsis), item)
		}
	} else {
		r := m.styles.Item.Render(m.items[itemID])
		item = ansi.Truncate(r, maxWidth, m.styles.Item.Inline(true).Render(constants.Ellipsis))
	}

	if m.showSeparators && itemID != len(m.items)-1 {
		return lipgloss.JoinHorizontal(lipgloss.Center, item, m.styles.Separator.Render(m.separator))
	}

	return item
}

func max(a, b int) int {
	if a > b {
		return a
	}

	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}

	return b
}

func clamp(v, low, high int) int {
	return min(max(v, low), high)
}
